package com.zuorenke.mybatis;

import com.zuorenke.db.extend.Dialect;
import com.zuorenke.db.extend.MySQLDialect;
import com.zuorenke.db.extend.Page;
import com.zuorenke.help.ReflectionUtils;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Properties;

/**
 * @author Z.R.K
 * @description
 * @create 2019-04-12 15:58:52
 **/
@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class PageLimitInterceptor implements Interceptor {
	
	private static final Logger logger = LoggerFactory.getLogger(PageLimitInterceptor.class);
	
	static final int MAPPED_STATEMENT_INDEX = 0;
	
	static final int PARAMETER_INDEX = 1;
	
	static final int ROWBOUNDS_INDEX = 2;
	
//	static final int RESULT_HANDLER_INDEX = 3;
	
	static final String SQL_NODE_ITEM_PREFIX = "__frch_";
	
	Dialect dialect = new MySQLDialect();
	
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		Object[] queryArgs = invocation.getArgs();
//		invocation.getMethod()
//		invocation.getArgs()
		processIntercept(queryArgs);
//		long start = System.currentTimeMillis();
		Object result = invocation.proceed();
		
//		long end = System.currentTimeMillis();
//		try{
//			SqlStatHandler.send((MappedStatement)queryArgs[MAPPED_STATEMENT_INDEX], queryArgs[PARAMETER_INDEX], start, end);
//		} catch (Throwable t) {
//			logger.error(t.getMessage(), t);
//		}
		
		return result;
	}
	
	private void processIntercept(final Object[] queryArgs) {
		
		MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
		Object parameter = queryArgs[PARAMETER_INDEX];
		final RowBounds rowBounds = (RowBounds) queryArgs[ROWBOUNDS_INDEX];
		
		BoundSql boundSql = ms.getBoundSql(parameter);
		String sql = boundSql.getSql().trim();//源sql语句
		
		if (rowBounds instanceof Page) {
			
			int offset = rowBounds.getOffset();
			int limit = rowBounds.getLimit();
			
			if (dialect.supportsLimit() && (offset != RowBounds.NO_ROW_OFFSET || limit != RowBounds.NO_ROW_LIMIT)) {
				if (rowBounds instanceof Page) {
					if(ms.getId().indexOf("Page") > -1){
						String countSql = dialect.getDefaultCountSql(sql);
						int totalCount = getTotalSize(countSql, ms, parameter, boundSql);
						((Page)rowBounds).setTotalSize(totalCount);
						offset = rowBounds.getOffset();
						limit = rowBounds.getLimit();
					}
				}
				
				if (dialect.supportsLimitOffset()) {
					sql = dialect.getLimitString(sql, offset, limit);
					offset = RowBounds.NO_ROW_OFFSET;
				} else {
					sql = dialect.getLimitString(sql, 0, limit);
				}
				
				limit = RowBounds.NO_ROW_LIMIT;
				
				//替换源sql语句
				queryArgs[ROWBOUNDS_INDEX] = new RowBounds(offset, limit);
				
				
			}
		}else {
			queryArgs[ROWBOUNDS_INDEX] = new RowBounds();
		}
		
		Object metaParameters=ReflectionUtils.getFieldValue(boundSql, "metaParameters");
		BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
		ReflectionUtils.setFieldValue(newBoundSql,"metaParameters",metaParameters);
		
		MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
		queryArgs[MAPPED_STATEMENT_INDEX] = newMs;
	}
	
	private int getTotalSize(String countSql, MappedStatement ms, Object parameter, BoundSql boundSql) {
		
		int totalSize = 0;
		BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
		
		MappedStatement countMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(countBoundSql));
		
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			conn = ms.getConfiguration().getEnvironment().getDataSource().getConnection();
			ps = conn.prepareStatement(countSql);
			setParameters(ps, countMs, boundSql, parameter);
			rs = ps.executeQuery();
			try {
				if (null != rs && rs.next()) {
					totalSize = rs.getInt(1);
				}
			} catch (SQLException sqle) {
				logger.error("parser and execute select count(*) sql failure!");
				throw new RuntimeException("parser and execute select count(*) sql failure!", sqle);
			}
		} catch (SQLException sqle) {
			logger.warn("Failed to excute page count sql:" + countSql, sqle);
		} finally {
			try {
				if (null != rs) {
					rs.close();
				}
				if (null != ps) {
					ps.close();
				}
				if (null != conn) {
					conn.close();
				}
			} catch (SQLException sqle) {
				logger.warn("Failed to close connection" + sqle.getMessage());
			}
		}
		
		return totalSize;
	}
	
	/**
	 * 对SQL参数(?)设值,参考org.apache.ibatis.executor.parameter.DefaultParameterHandler
	 * @param ps
	 * @param mappedStatement
	 * @param boundSql
	 * @param parameterObject
	 * @throws SQLException
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException {
		ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		if (parameterMappings != null) {
			Configuration configuration = mappedStatement.getConfiguration();
			TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
			MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
			for (int i = 0; i < parameterMappings.size(); i++) {
				ParameterMapping parameterMapping = parameterMappings.get(i);
				if (parameterMapping.getMode() != ParameterMode.OUT) {
					Object value;
					String propertyName = parameterMapping.getProperty();
					PropertyTokenizer prop = new PropertyTokenizer(propertyName);
					if (parameterObject == null) {
						value = null;
					} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
						value = parameterObject;
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						value = boundSql.getAdditionalParameter(propertyName);
					} else if (propertyName.startsWith(SQL_NODE_ITEM_PREFIX) && boundSql.hasAdditionalParameter(prop.getName())) {
						value = boundSql.getAdditionalParameter(prop.getName());
						if (value != null) {
							value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
						}
					} else {
						value = metaObject == null ? null : metaObject.getValue(propertyName);
					}
					TypeHandler typeHandler = parameterMapping.getTypeHandler();
					if (typeHandler == null) {
						logger.error("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
						throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
					}
					typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());
				}
			}
		}
	}
	
	/**
	 *
	 * 描述: 构建一个新的MappedStatement
	 * @param ms 传入的MappedStatement
	 * @param newSqlSource sqlSource
	 * @return MappedStatement MappedStatement
	 */
	private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
		MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		
		builder.timeout(ms.getTimeout());
		
		builder.parameterMap(ms.getParameterMap());
		
		builder.resultMaps(ms.getResultMaps());
		builder.resultSetType(ms.getResultSetType());
		
		builder.cache(ms.getCache());
		builder.flushCacheRequired(ms.isFlushCacheRequired());
		builder.useCache(ms.isUseCache());
		
		return builder.build();
	}
	
	/**
	 * 注册插件信息
	 */
	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}
	
	/**
	 * 获取到当前所配置的适配器对象
	 */
	@Override
	public void setProperties(Properties properties) {
		String dialectClass = properties.getProperty("dialectClass");
		if (dialectClass == null) {
			throw new NoSuchElementException("dialectClass");
		}
		try {
			dialect = (Dialect) Class.forName(dialectClass).newInstance();
		} catch (Exception e) {
			logger.error("cannot create dialect instance by dialectClass:" + dialectClass, e);
			throw new RuntimeException("cannot create dialect instance by dialectClass:" + dialectClass, e);
		}
	}
	
	public static class BoundSqlSqlSource implements SqlSource {
		BoundSql boundSql;
		
		public BoundSqlSqlSource(BoundSql boundSql) {
			this.boundSql = boundSql;
		}
		
		@Override
		public BoundSql getBoundSql(Object parameterObject) {
			return boundSql;
		}
	}
}